SPB Git

spb/worthdoing Public

Autonomous investigation agent that discovers, challenges, and ranks things genuinely worth doing — Claude + Firecrawl, Next.js 16, PostgreSQL

TypeScript 91.5% SQL 5.8% CSS 2.2%
3.5 KB · 101 lines typescript
Raw Blame History
1/**2 * WorthDoing.ai3 * Author: Simon-Pierre Boucher4 * Contact: contact@spboucher.ai5 * File: src/app/api/opportunities/[id]/route.ts6 * Description: Full opportunity report data — report, scores, evidence with sources, competitors, skeptic case.7 */8import { NextRequest, NextResponse } from "next/server";9import { and, asc, eq, inArray } from "drizzle-orm";10import { z } from "zod";11import { db } from "@/lib/db/client";12import {13  opportunities,14  opportunityScores,15  opportunityEvidence,16  opportunityCompetitors,17  competitors,18  evidence,19  sources,20  investigations,21  hypotheses,22} from "@/lib/db/schema";2324export const dynamic = "force-dynamic";2526export async function GET(_req: NextRequest, ctx: { params: Promise<{ id: string }> }) {27  const { id } = await ctx.params;28  if (!z.string().uuid().safeParse(id).success) {29    return NextResponse.json({ error: "Invalid opportunity id." }, { status: 400 });30  }3132  const [opp] = await db.select().from(opportunities).where(eq(opportunities.id, id));33  if (!opp) return NextResponse.json({ error: "Opportunity not found." }, { status: 404 });3435  const [inv] = await db.select().from(investigations).where(eq(investigations.id, opp.investigationId));36  const hyp = opp.hypothesisId37    ? (await db.select().from(hypotheses).where(eq(hypotheses.id, opp.hypothesisId)))[0]38    : null;3940  const scores = await db.select().from(opportunityScores).where(eq(opportunityScores.opportunityId, id));4142  const links = await db.select().from(opportunityEvidence).where(eq(opportunityEvidence.opportunityId, id));43  const evidenceIds = links.map((l) => l.evidenceId);44  const evRows = evidenceIds.length45    ? await db46        .select({47          id: evidence.id,48          kind: evidence.kind,49          quote: evidence.quote,50          summary: evidence.summary,51          strength: evidence.strength,52          createdAt: evidence.createdAt,53          sourceUrl: sources.canonicalUrl,54          sourceTitle: sources.title,55          sourceDomain: sources.domain,56        })57        .from(evidence)58        .innerJoin(sources, eq(sources.id, evidence.sourceId))59        .where(and(eq(evidence.investigationId, opp.investigationId), inArray(evidence.id, evidenceIds)))60        .orderBy(asc(evidence.createdAt))61    : [];62  const roleByEvidence = new Map(links.map((l) => [l.evidenceId, l.role]));6364  const comps = await db65    .select({66      name: competitors.name,67      url: competitors.url,68      description: competitors.description,69      note: opportunityCompetitors.note,70    })71    .from(opportunityCompetitors)72    .innerJoin(competitors, eq(competitors.id, opportunityCompetitors.competitorId))73    .where(eq(opportunityCompetitors.opportunityId, id));7475  return NextResponse.json({76    opportunity: {77      id: opp.id,78      title: opp.title,79      summary: opp.summary,80      problem: opp.problem,81      whyNow: opp.whyNow,82      risks: opp.risks,83      skepticCase: opp.skepticCase,84      reportMd: opp.reportMd,85      status: opp.status,86      worthScore: opp.worthScore,87      evidenceConfidence: opp.evidenceConfidence,88      createdAt: opp.createdAt,89    },90    investigation: inv91      ? { id: inv.id, objective: inv.objective, status: inv.status, completedAt: inv.completedAt }92      : null,93    hypothesis: hyp94      ? { id: hyp.id, title: hyp.title, statement: hyp.statement, status: hyp.status, confidence: hyp.confidence }95      : null,96    scores,97    evidence: evRows.map((e, i) => ({ ...e, index: i + 1, role: roleByEvidence.get(e.id) ?? "context" })),98    competitors: comps,99  });100}101